﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Windows.Markup;

using agree.schema;

using alib.Collections;
using alib.Collections.ReadOnly;
using alib.Debugging;
using alib.Enumerable;
using alib.String;

namespace agree
{
	///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	/// <summary>
	/// Helper class for assembling disjoint streams of TDL tokens into token definition groups which represent a single
	/// typed feature structure. The sailing through this segment is not quite as smooth due to historical warts. Alas
	/// in past times, the pipeline itself nominally manifested no storage, such that it was ultimately the BCPO computation 
	/// that spun up demand for TDL tokens.
	/// </summary>
	///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	[ContentProperty("TdlTokenStream")]
	[UsableDuringInitialization(true)]
	public class TdlConstraintGrouper : m_observable<IObservable<TdlTok>, IdentifierTokenGroup>,
		IObserver<TdlTok>,
		ρρTdlHash,
		ρρTdlTokenStream,
		ρρTdlType
	{
		public TdlConstraintGrouper(IRtParent parent, IObservable<TdlTok> κκSrc, TdlType type)
			: base(parent, κκSrc)
		{
			this.tdl_type = type;
		}
		public TdlConstraintGrouper(IRtParent parent, IObservable<TdlTok> κκSrc)
			: this(parent, κκSrc, TdlType.Type)
		{
		}
		public TdlConstraintGrouper(IRtParent parent)
			: this(parent, default(IObservable<TdlTok>))
		{
		}


		///////////////////////////////////////////////////////////////////////
		/// 
		[DebuggerBrowsable(DebuggerBrowsableState.Never)]
		public IObservable<TdlTok> TdlTokenStream
		{
			get { return this.t0; }
			set { this.t0 = value; }
		}
		/// 
		///////////////////////////////////////////////////////////////////////


		///////////////////////////////////////////////////////////////////////
		/// 
		public long TdlHash
		{
			get
			{
				ρρTdlHash ths;
				return (ths = this.t0 as ρρTdlHash) == null ? 0 : ths.TdlHash;
			}
		}
		/// 
		///////////////////////////////////////////////////////////////////////


		///////////////////////////////////////////////////////////////////////
		/// 
		[DefaultValue(typeof(TdlType), "global::agree.TdlType.Type")]
		public TdlType TdlType
		{
			get { return this.tdl_type; }
			set { this.tdl_type = value; }
		}
		/// 
		///////////////////////////////////////////////////////////////////////


		///////////////////////////////////////////////////////////////////////
		/// 
		public bool ConstraintBodyOnly;
		/// 
		///////////////////////////////////////////////////////////////////////



		[DebuggerBrowsable(DebuggerBrowsableState.Never)]
		[AccessedThroughProperty("TdlType")]
		TdlType tdl_type;

		[DebuggerBrowsable(DebuggerBrowsableState.Never)]
		int src_seq_ctr;


		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		/// I. gather
		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

		List<TdlTok> gather;	/// TODO - this is unfortunate and temporary, I hope. It is a holding area between 
		/// the two phases of work and could be a bottleneck

		protected override void start(IObservable<TdlTok> toks)
		{
			gather = new List<TdlTok>();
			toks.Subscribe(this);
		}

		void IObserver<TdlTok>.OnNext(TdlTok value)
		{
			gather.Add(value);
		}
		void IObserver<TdlTok>.OnCompleted()
		{
			finish();
			_remove_task();
		}

		void IObserver<TdlTok>.OnError(Exception error) { _error(error); }

		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		/// II. replay
		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

		[DebuggerBrowsable(DebuggerBrowsableState.Never)]
		IEnumerator<TdlTok> replay;

		[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
		Stack<TdlTok> token_stack;

		bool f_inhibit_next_advance;

		TdlTok prv, cur;

		public void Reset()
		{
			prv = cur = default(TdlTok);
			f_inhibit_next_advance = default(bool);
			token_stack = new Stack<TdlTok>();
			replay = gather.GetEnumerator();
		}

		bool Advance()
		{
			if (f_inhibit_next_advance)
				f_inhibit_next_advance = false;
			else if (!replay.MoveNext())
				return false;
			prv = cur;
			cur = replay.Current;
			return true;
		}

		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		/// <summary>
		/// Groups tokens from a token stream into zero or more token definition groups.
		/// </summary>
		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		void finish()
		{
			Reset();
			while (Advance())
				foreach (var itg in build_token_groups(assignment_group(cur)))
					_item(itg);
		}

		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		/// 
		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		IdentifierTokenGroup assignment_group(TdlTok ident)
		{
			bool f_append = false;
			IdentifierTokenGroup tdg = null;

			if (!ConstraintBodyOnly)
			{
				if (ident.type_id != TdlTok.Type.Identifier)
					TdlTokenizer.ErrorExit(ident, "Expected an identifier.");

				if (!Advance())
					TdlTokenizer.ErrorExit(ident, "Unexpected end of file.");

				if ((cur.type_id & (TdlTok.Type.Define | TdlTok.Type.Append)) == 0)
					ErrorExit("Expected := or :+");

				f_append = (cur.type_id == TdlTok.Type.Append);

				if (!Advance())
					ErrorExit("Unexpected end of file in type definition.");

				if (cur.type_id == TdlTok.Type.Morphology)
					tdg = morphology_group(ident, f_append);
			}
			return tdg ?? new IdentifierTokenGroup(tdl_type, ident, src_seq_ctr++, f_append);
		}

		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		/// 
		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		MorphologyIdentifierTokenGroup morphology_group(TdlTok ident, bool f_append)
		{
			var mtdg = new MorphologyIdentifierTokenGroup(tdl_type, ident, src_seq_ctr++, f_append);
			do
			{
				/// Although supposedly forbidden by the LKB docs (Copestake 2002, p.131) some grammars get away with
				/// using morphology specifications in LexicalRule files.
				if (tdl_type != TdlType.LexicalRule)
					ErrorExit("Cannot specify inflection rules in this file.");

				mtdg.morph_subrules.AddRange(((TdlTokMorphology)cur).subrules);

				if (!Advance())
					ErrorExit("Unexpected end of file after '%prefix' or '%suffix' in inflectional rule definition.");
			}
			while (cur.type_id == TdlTok.Type.Morphology);
			return mtdg;
		}


		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		/// <summary>
		/// A preliminary rough examination of a TDL token stream, examining it in only enough detail to accurately find
		/// the boundaries between the constraint definitions. We also record the number of parts in a certain type of
		/// (obscure) list notation, because doing so here eliminates some unwanted during the parsing-in-earnest.
		/// </summary>
		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		void ScanConstraint(IdentifierTokenGroup itg)
		{
			TdlTokenList out_toks = null;
			ConsDefs cd = itg.constraints;

			token_stack.Push(cur);

			while (true)
			{
				if (!Advance())
					ErrorExit("End of file searching for ']' to terminate feature structure. Unmatched '['.");

				if (cur.type_id == TdlTok.Type.Comma)
				{
					if (out_toks == null)
						ErrorExit("Invalid ',', {0} cannot have empty parts.", token_stack.Peek());

					if (token_stack.Count == 1)	// must be '['
					{
						cd.Add(out_toks);
						out_toks = null;
						continue;
					}
				}
				else if (cur.type_id == TdlTok.Type.Dot)
				{
					if (out_toks == null)
						ErrorExit("Invalid '.', {0} cannot have empty parts.", token_stack.Peek());

				}
				else if ((cur.type_id & TdlTok.Type.TokMap_AnyOpen) != 0)
					token_stack.Push(cur);
				else if ((cur.type_id & TdlTok.Type.TokMap_AnyClose) != 0)
				{
					if (token_stack.Count == 0)
						ErrorExit("List not open; '{0}' is invalid.", cur);

					// the pending (opening) grouping token
					TdlTok tok_open = token_stack.Pop();

					// hacky way to get the expected closing token corresponding to the actual opening token
					TdlTok.Type expecting = (TdlTok.Type)((int)tok_open.type_id << 1);

					// check that the opening token for the encountered closing token matches the opening token for the
					// pending grouping type
					if (cur.type_id != expecting)
						ErrorExit("Invalid nesting '{0}', expected {1}.", cur, expecting);

					// disallow empty parts
					if (prv.type_id == TdlTok.Type.Comma)
						ErrorExit("Invalid ',', list {0} {1} cannot have empty parts.", tok_open, cur);

					if (token_stack.Count == 0 && cur.type_id == TdlTok.Type.SquareClose)
						break;
				}
				if (out_toks == null)
					out_toks = new TdlTokenList();
				out_toks.Add(cur);
			}
			cd.Add(out_toks);
		}

		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		/// <summary>
		/// 
		/// </summary>
		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		IEnumerable<IdentifierTokenGroup> build_token_groups(IdentifierTokenGroup itg)
		{
			token_stack.Clear();
			while (true)
			{
				switch (cur.type_id)
				{
					case TdlTok.Type.SquareOpen:
						ScanConstraint(itg);
						break;

					case TdlTok.Type.String:
						itg.top_comment = cur.Text;
						break;

					case TdlTok.Type.Identifier:
						itg.κκParents.Add(cur);
						break;

					case TdlTok.Type.Integer:
						throw ErrorExit("Integer value '{0}' is not valid at the tol level of a TDL definition.", _CurTok);

					case TdlTok.Type.Tag:
						throw ErrorExit("Cannot conjoin a co-reference tag (#{0}) at the top level of a TDL definition.", _CurTok);

					case TdlTok.Type.Morphology:
						throw ErrorExit("Inflectional prefix or suffix must occur after ':=' and before any parent types.");

					default:
						throw ErrorExit("Unexpected token '{0}'.", _CurTok);
				}

				if (!Advance())
					ErrorExit("Unexpected end of file in type definition.");

				switch (cur.type_id)
				{
					case TdlTok.Type.SquareOpen:
						// mirror odd LKB behavior -- allow top-level feature structure without conjunction
						//SysObj.Instance.TransactionStatus(itg, cur.FilePos, "Warning: accepting non-standard TDL; top-level constraint on '{0}' should be conjoined with the '&' symbol.", _CurTok);
						f_inhibit_next_advance = true;
						break;

					case TdlTok.Type.Ampersand:
						break;

					case TdlTok.Type.Dot:
						// all TDL definitions must be terminated with a period
						itg.constraints.cch = (cur.FilePos - itg.constraints.identifier.FilePos) + 1;
						yield return itg;
						yield break;

					default:
						throw ErrorExit("'{0}' is not valid here. Expected '&' or '.'", cur);
				}

				if (!Advance())
					ErrorExit("Unexpected end of file in type definition.");
			}
		}

		[DebuggerBrowsable(DebuggerBrowsableState.Never)]
		const Object _CurTok = null;	/// placeholder for error reporting in the args of the following function
		[DebuggerStepThrough]
		Exception ErrorExit(String fmt, params Object[] args)
		{
			if (args != null && args[0] == null)
				args[0] = cur.Text ?? prv.Text;
			throw TdlTokenizer.ErrorExit(cur, fmt, args);
		}
	};
}
